program ManagedLights;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Math,
  SampleCommon,
  DIrr in '..\..\DSource\DIrr.pas',
  DIrr.GUI in '..\..\DSource\DIrr.GUI.pas',
  DIrr.Color in '..\..\DSource\DIrr.Color.pas',
  DIrr.Rect in '..\..\DSource\DIrr.Rect.pas',
  DIrr.Event in '..\..\DSource\DIrr.Event.pas',
  DIrr.Vector2D in '..\..\DSource\DIrr.Vector2D.pas',
  DIrr.Vector3D in '..\..\DSource\DIrr.Vector3D.pas',
  DIrr.Video in '..\..\DSource\DIrr.Video.pas',
  DIrr.Base in '..\..\DSource\DIrr.Base.pas',
  DIrr.Types in '..\..\DSource\DIrr.Types.pas',
  DIrr.Scene in '..\..\DSource\DIrr.Scene.pas',
  DIrr.Structs in '..\..\DSource\DIrr.Structs.pas',
  DIrr.Aabbox3D in '..\..\DSource\DIrr.Aabbox3D.pas',
  DIrr.Matrix4 in '..\..\DSource\DIrr.Matrix4.pas',
  DIrr.Texture in '..\..\DSource\DIrr.Texture.pas',
  DIrr.Dimension2d in '..\..\DSource\DIrr.Dimension2D.pas',
  DIrr.Collections in '..\..\DSource\DIrr.Collections.pas',
  DIrr.ConcreteMesh in '..\..\DSource\DIrr.ConcreteMesh.pas',
  DIrr.Quake3 in '..\..\DSource\DIrr.Quake3.pas',
  DIrr.Buffer in '..\..\DSource\DIrr.Buffer.pas',
  DIrr.IO in '..\..\DSource\DIrr.IO.pas',
  DIrr.Line3D in '..\..\DSource\DIrr.Line3D.pas',
  DIrr.Plane3D in '..\..\DSource\DIrr.Plane3D.pas',
  DIrr.Math in '..\..\DSource\DIrr.Math.pas',
  DIrr.Triangle3D in '..\..\DSource\DIrr.Triangle3D.pas',
  DIrr.Consts in '..\..\DSource\DIrr.Consts.pas',
  System.Generics.Defaults,
  System.Generics.Collections;

(*
    Normally, you are limited to 8 dynamic lights per scene: this is a hardware limit.  If you
    want to use more dynamic lights in your scene, then you can register an optional light
    manager that allows you to to turn lights on and off at specific point during rendering.
    You are still limited to 8 lights, but the limit is per scene node.

    This is completely optional: if you do not register a light manager, then a default
    distance-based scheme will be used to prioritise hardware lights based on their distance
    from the active camera.

	NO_MANAGEMENT disables the light manager and shows Irrlicht's default light behaviour.
    The 8 lights nearest to the camera will be turned on, and other lights will be turned off.
    In this example, this produces a funky looking but incoherent light display.

	LIGHTS_NEAREST_NODE shows an implementation that turns on a limited number of lights
    per mesh scene node.  If finds the 3 lights that are nearest to the node being rendered,
    and turns them on, turning all other lights off.  This works, but as it operates on every
    light for every node, it does not scale well with many lights.  The flickering you can see
    in this demo is due to the lights swapping their relative positions from the cubes
    (a deliberate demonstration of the limitations of this technique).

	LIGHTS_IN_ZONE shows a technique for turning on lights based on a 'zone'. Each empty scene
    node is considered to be the parent of a zone.  When nodes are rendered, they turn off all
    lights, then find their parent 'zone' and turn on all lights that are inside that zone, i.e.
	are  descendents of it in the scene graph.  This produces true 'local' lighting for each cube
    in this example.  You could use a similar technique to locally light all meshes in (e.g.)
    a room, without the lights spilling out to other rooms.

	This light manager is also an event receiver; this is purely for simplicity in this example,
    it's neither necessary nor recommended for a real application.
*)
type
  TMyLightManager = class(TLightManager)
  private type
    TLightManagementMode = (lmmNoManagement, lmmLightsNearestNode, lmmLightsInZone);

    // A utility class to aid in sorting scene nodes into a distance order
    TLightDistanceElement = class
    private
      FNode: TSceneNode;
      FDistance: F64;
    private type
      TLDEComparer = class(TInterfacedObject, IComparer<TLightDistanceElement>)
      private
        function Compare(const Left, Right: TLightDistanceElement): Integer;
      end;
    public
      constructor Create; overload;
      constructor Create(Node: TSceneNode; d: F64); overload;
    public
      class function GetComparer: IComparer<TLightDistanceElement>; inline; static;
    public
      property Distance: F64 read FDistance write FDistance;
      property ConcreteNode: TSceneNode read FNode write FNode;
    end;

  private
    FMode: TLightManagementMode;
    FRequestedMode: TLightManagementMode;

    // These data represent the state information that this light manager
    // is interested in.
    FSceneManager: TSceneManager;

    FSceneLightListWrap: TSceneNodeArray;

    FCurrentRenderPass: E_SCENE_NODE_RENDER_PASS;
    FCurrentSceneNode: TSceneNode;
  private
    function FindZone(ANode: TSceneNode): TSceneNode;
    procedure TurnOnZoneLights(ANode: TSceneNode);
  public
    constructor Create(SceneMgr: TSceneManager);

    // The input receiver interface, which just switches light management strategy
    function OnEvent(const Event: SEvent): Boolean; override;
  public
    procedure OnPreRender(LightList: TSceneNodeArray); override;
    procedure OnPostRender; override;
    procedure OnRenderPassPreRender(RenderPass: E_SCENE_NODE_RENDER_PASS); override;
    procedure OnRenderPassPostRender(RenderPass: E_SCENE_NODE_RENDER_PASS); override;
    procedure OnNodePreRender(ANode: TSceneNode); override;
    procedure OnNodePostRender(ANode: TSceneNode); override;
  end;

var
  DriverType: E_DRIVER_TYPE;
  Device: TIrrlichtDevice;
  Smgr: TSceneManager;
  Driver: TVideoDriver;
  Camera: TCameraSceneNode;

  TmpStr: String;
  LightRadius: F32 = 60;
  GuiEnv: TGUIEnvironment;
  Skin: TGUISkin;
  Font: TGUIFont;
  ZoneX: F32 = -100;
  ZoneY: F32 = -60;
  ZoneRoot: TSceneNode;
  MeshNode: TMeshSceneNode;
  Rotation: TSceneNodeAnimator;
  Billborad: TBillboardSceneNode;
  MyLightManager: TMyLightManager;

  LastFps: Integer = -1;
  Fps: Integer;

{ TMyLightManager }

constructor TMyLightManager.Create(SceneMgr: TSceneManager);
begin
  FSceneManager := SceneMgr;
end;

// Find the empty scene node that is the parent of the specified node
function TMyLightManager.FindZone(ANode: TSceneNode): TSceneNode;
begin
  if Assigned(ANode) then
  begin
    if ANode.GetType = ESNT_EMPTY then
      Result := ANode
    else
      Result := FindZone(ANode.GetParent);
  end
  else
  begin
    Result := nil;
  end;
end;

function TMyLightManager.OnEvent(const Event: SEvent): Boolean;
begin
  Result := False;

  if (Event.EventType = EET_KEY_INPUT_EVENT) and (Event.KeyInput.PressedDown) then
  begin
    Result := True;
    case Event.KeyInput.Key of
      KEY_KEY_1: FRequestedMode := lmmNoManagement;
      KEY_KEY_2: FRequestedMode := lmmLightsNearestNode;
      KEY_KEY_3: FRequestedMode := lmmLightsInZone;
      else
        Result := False;
    end;

    if FRequestedMode = lmmNoManagement then
      FSceneManager.SetLightManager(nil)
    else
      FSceneManager.SetLightManager(Self);
  end;
end;

// Called after the specified scene node is rendered
procedure TMyLightManager.OnNodePostRender(ANode: TSceneNode);
begin
  // I don't need to do any light management after individual node rendering.
end;

// This is called before the specified scene node is rendered
procedure TMyLightManager.OnNodePreRender(ANode: TSceneNode);
var
  i, lCount: S32;
  ANodePosition: TVector3DF;
  DistanceArray: TArray<TLightDistanceElement>;
  LightNode: TLightSceneNode;
  LightPosition: TVector3DF;
  LightDistance: F64;
  LightData: SLight;
  ParentZone, TmpNode: TSceneNode;
begin
  FCurrentSceneNode := ANode;

  // This light manager only considers solid objects, but you are free to manipulate
  // lights during any phase, depending on your requirements.
  if ESNRP_SOLID <> FCurrentRenderPass then
    Exit;

  // And in fact for this example, I only want to consider lighting for cube scene
  // nodes.  You will probably want to deal with lighting for (at least) mesh /
  // animated mesh scene nodes as well.
  if FMode = lmmLightsNearestNode then
  begin
    // This is a naive implementation that prioritises every light in the scene
    // by its proximity to the node being rendered.  This produces some flickering
    // when lights orbit closer to a cube than its 'zone' lights.
    ANode.GetAbsolutePosition(ANodePosition);

    // Sort the light list by prioritising them based on their distance from the node
    // that's about to be rendered.
    lCount := FSceneLightListWrap.Count;
    SetLength(DistanceArray, lCount);
    for i := 0 to lCount - 1 do
    begin
      LightNode := TLightSceneNode(FSceneLightListWrap.Item[i]^);
      LightNode.GetAbsolutePosition(LightPosition);
      LightDistance := LightPosition.GetDistanceFromSquared(ANodePosition);
      DistanceArray[i] := TLightDistanceElement.Create(LightNode, LightDistance);
    end;

    TArray.Sort<TLightDistanceElement>(DistanceArray, TLightDistanceElement.GetComparer);

    // The list is now sorted by proximity to the node.
    // Turn on the three nearest lights, and turn the others off.
    for i := 0 to lCount - 1 do
      DistanceArray[i].ConcreteNode.SetVisible(i < 3);

    for i := 0 to lCount - 1 do
      DistanceArray[i].Free;
    SetLength(DistanceArray, 0);
  end
  else if FMode = lmmLightsInZone then
  begin
    // Empty scene nodes are used to represent 'zones'.  For each solid mesh that
    // is being rendered, turn off all lights, then find its 'zone' parent, and turn
    // on all lights that are found under that node in the scene graph.
    // This is a general purpose algorithm that doesn't use any special
    // knowledge of how this particular scene graph is organised.
    lCount := FSceneLightListWrap.GetCount;
    for i := 0 to lCount - 1 do
    begin
      TmpNode := FSceneLightListWrap.Item[i]^;

      if TmpNode.GetType <> ESNT_LIGHT then
        Continue;

      LightNode := TLightSceneNode(TmpNode);
      LightData := LightNode.GetLightDataPtr^;

      if LightData.LightType <> ELT_DIRECTIONAL then
        LightNode.SetVisible(False);
    end;

    ParentZone := FindZone(ANode);
    if Assigned(ParentZone) then
      TurnOnZoneLights(ParentZone);
  end;

end;

procedure TMyLightManager.OnPostRender;
var
  i, lCount: S32;
  ANode: TSceneNode;
begin
  // Since light management might be switched off in the event handler, we'll turn all
  // lights on to ensure that they are in a consistent state. You wouldn't normally have
  // to do this when using a light manager, since you'd continue to do light management
  // yourself.
  lCount := FSceneLightListWrap.GetCount;
  for I := 0 to lCount - 1 do
  begin
    ANode := FSceneLightListWrap.Item[i]^;
    ANode.SetVisible(True);
  end;
end;

// This is called before the first scene node is rendered.
procedure TMyLightManager.OnPreRender(LightList: TSceneNodeArray);
begin
  // Update the mode; changing it here ensures that it's consistent throughout a render
  FMode := FRequestedMode;

  // Store the light list. I am free to alter this list until the end of OnPostRender().
  //SceneLightList = lightList;
  FSceneLightListWrap := LightList;
end;

procedure TMyLightManager.OnRenderPassPostRender(
  RenderPass: E_SCENE_NODE_RENDER_PASS);
var
  i, lCount: S32;
  ANode: TSceneNode;
begin
  if ESNRP_SOLID = RenderPass then
  begin
    lCount := FSceneLightListWrap.Count;
    for i := 0 to lCount - 1 do
    begin
      ANode := FSceneLightListWrap.Item[i]^;
      ANode.SetVisible(True);
    end;
  end;
end;

procedure TMyLightManager.OnRenderPassPreRender(
  RenderPass: E_SCENE_NODE_RENDER_PASS);
begin
  // I don't have to do anything here except remember which render pass I am in.
  FCurrentRenderPass := RenderPass;
end;

procedure TMyLightManager.TurnOnZoneLights(ANode: TSceneNode);
var
  NodeListPtr: TSceneNode.PSceneNodeList;
  Child: TNamedObject;
begin
  NodeListPtr := ANode.GetChildren;
  for Child in NodeListPtr^ do
  begin
    if (TSceneNode(Child).GetType = ESNT_LIGHT) then
      TSceneNode(Child).SetVisible(True)
    else  // Assume that lights don't have any children that are also lights
      TurnOnZoneLights(TSceneNode(Child));
  end;

end;

{ TMyLightManager.TLightDistanceElement.TLDEComparer }

function TMyLightManager.TLightDistanceElement.TLDEComparer.Compare(const Left,
  Right: TLightDistanceElement): Integer;
begin
  if Left.FDistance < Right.FDistance then
    Result := -1
  else if Left.FDistance > Right.FDistance then
    Result := 1
  else
    Result := 0;
end;

{ TMyLightManager.TLightDistanceElement }

constructor TMyLightManager.TLightDistanceElement.Create;
begin

end;

constructor TMyLightManager.TLightDistanceElement.Create(Node: TSceneNode;
  d: F64);
begin
  FNode := Node;
  FDistance := d;
end;

class function TMyLightManager.TLightDistanceElement.GetComparer: IComparer<TLightDistanceElement>;
begin
  Result := TLDEComparer.Create;
end;

begin
  try
    ReportMemoryLeaksOnShutdown := True;

    // ask user for driver
    DriverType := DriverChoiceConsole;
    if DriverType = EDT_COUNT then
    begin
      ExitCode := 1;
      Exit;
    end;

    //Initialise the engine
    Device := CreateDevice(DriverType, Dimension2DU(640, 480), 32);
    if not Assigned(Device) then
    begin
      ExitCode := 1;
      Exit;
    end;

    // Enough to reach the far side of each 'zone'
    Driver := Device.GetVideoDriver;
    Smgr := Device.GetSceneManager;
    GuiEnv := Device.GetGUIEnvironment;

    Skin := GuiEnv.GetSkin;
    if Assigned(Skin) then
    begin
      Skin.SetColor(EGDC_BUTTON_TEXT, IrrColor(255, 255, 255));
      Font := GuiEnv.GetFont('..\..\media\fontlucida.png');
      if Assigned(Font) then
        Skin.SetFont(Font);
    end;

    GuiEnv.AddStaticText('1 - No light management', IrrRect(10, 10, 200, 30));
    GuiEnv.AddStaticText('2 - Closest 3 lights', IrrRect(10, 30, 200, 50));
    GuiEnv.AddStaticText('3 - Lights in zone', IrrRect(10, 50, 200, 70));

    (*
    Add several "zones".  You could use this technique to light individual rooms, for example.
    *)
    ZoneX := -100;
    while ZoneX <= 100 do
    begin
      ZoneY := -60;
      while ZoneY <= 60 do
      begin
        // Start with an empty scene node, which we will use to represent a zone.
        ZoneRoot := Smgr.AddEmptySceneNode();
        ZoneRoot.SetPosition(Vector3DF(ZoneX, ZoneY, 0));

        // Each zone contains a rotating cube
        MeshNode := Smgr.AddCubeSceneNode(15, ZoneRoot);
        Rotation := Smgr.CreateRotationAnimator(Vector3DF(0.25, 0.5, 0.75));
        MeshNode.AddAnimator(Rotation);
        Rotation.Drop;

        // And each cube has three lights attached to it.  The lights are attached to billboards so
        // that we can see where they are.  The billboards are attached to the cube, so that the
        // lights are indirect descendents of the same empty scene node as the cube.
        Billborad := Smgr.AddBillboardSceneNode(MeshNode);
        Billborad.SetPosition(Vector3DF(0, -14, 30));
        Billborad.SetMaterialType(EMT_TRANSPARENT_ADD_COLOR);
        Billborad.SetMaterialTexture(0, Driver.GetTexture('..\..\media\particle.bmp'));
        Billborad.SetMaterialFlag(EMF_LIGHTING, False);
        Smgr.AddLightSceneNode(Billborad, Vector3DF(0, 0, 0), IrrColorF(1, 0, 0), LightRadius);

        Billborad := Smgr.AddBillboardSceneNode(MeshNode);
        Billborad.SetPosition(Vector3DF(-21, -14, -21));
        Billborad.SetMaterialType(EMT_TRANSPARENT_ADD_COLOR);
        Billborad.SetMaterialTexture(0, Driver.GetTexture('..\..\media\particle.bmp'));
        Billborad.SetMaterialFlag(EMF_LIGHTING, False);
        Smgr.AddLightSceneNode(Billborad, VECTOR3DF_0, IrrColorF(0, 1, 0), LightRadius);

        Billborad := Smgr.AddBillboardSceneNode(MeshNode);
        Billborad.SetPosition(Vector3DF(21, -14, -21));
        Billborad.SetMaterialType(EMT_TRANSPARENT_ADD_COLOR);
        Billborad.SetMaterialTexture(0, Driver.GetTexture('..\..\media\particle.bmp'));
        Billborad.SetMaterialFlag(EMF_LIGHTING, False);
        Smgr.AddLightSceneNode(Billborad, VECTOR3DF_0, IrrColorF(0, 0, 1), LightRadius);

        // Each cube also has a smaller cube rotating around it, to show that the cubes are being
        // lit by the lights in their 'zone', not just lights that are their direct children.
        MeshNode := Smgr.AddCubeSceneNode(5, MeshNode);
        MeshNode.SetPosition(Vector3DF(0, 21, 0));

        ZoneY := ZoneY + 60;
      end;
      ZoneX := ZoneX + 50;
    end;

    Smgr.AddCameraSceneNode(nil, Vector3DF(0, 0, -130), VECTOR3DF_0);

    MyLightManager := TMyLightManager.Create(Smgr);
    MyLightManager.Grab;

    Smgr.SetLightManager(nil);  // This is the default: we won't do light management until told to do it.
    Device.SetEventReceiver(MyLightManager);

    while Device.Run do
    begin
      Driver.BeginScene(True, True, IrrColor(100, 101, 140));
      Smgr.DrawAll;
      GuiEnv.DrawAll;
      Driver.EndScene;

      FPS := Driver.GetFPS;
      if FPS <> LastFPS then
      begin
        LastFPS := FPS;
        TmpStr := 'Managed Lights [' + String(Driver.GetName) + '] FPS:';
        TmpStr := TmpStr + IntToStr(FPS);
        Device.SetWindowCaption(PChar(TmpStr));
      end;

    end;

    Device.Drop;

    MyLightManager.Drop;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
